/*  XMMS - Cross-platform multimedia player
 *  Copyright (C) 1998-2003  Peter Alm, Mikael Alm, Olle Hallnas,
 *                           Thomas Nilsson and 4Front Technologies
 *  Copyright (C) 1999-2006  Haavard Kvaalen <havardk@xmms.org>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "cdaudio.h"
#include <pthread.h>
#include <errno.h>
#include "xmms/i18n.h"
#include "libxmms/util.h"
#include "libxmms/titlestring.h"

#ifdef CDROMSTOP
# define XMMS_STOP CDROMSTOP
#elif defined CDIOCSTOP
# define XMMS_STOP CDIOCSTOP
#else
# error "No stop ioctl"
#endif

#ifdef CDIOCPAUSE
# define XMMS_PAUSE CDIOCPAUSE
#elif defined CDROMPAUSE
# define XMMS_PAUSE CDROMPAUSE
#else
# error "No pause ioctl"
#endif

#ifdef CDIOCRESUME
# define XMMS_RESUME CDIOCRESUME
#elif defined CDROMRESUME
# define XMMS_RESUME CDROMRESUME
#else
# error "No resume ioctl"
#endif

/*
 * Distributions should not patch this, but instead use the
 * --with-cdda-device=path and --with-cdda-dir=path configure options.
 */

#ifndef CDDA_DEVICE
# ifdef HAVE_SYS_CDIO_H
#  ifdef __FreeBSD__
#   define CDDA_DEVICE "/dev/acd0"
#  elif defined __OpenBSD__
#   define CDDA_DEVICE "/dev/cd0c"
#  else
#   define CDDA_DEVICE "/vol/dev/aliases/cdrom0"
#  endif
# else
#   define CDDA_DEVICE "/dev/cdrom"
# endif
#endif

#ifndef CDDA_DIRECTORY
# ifdef HAVE_SYS_CDIO_H
#  ifdef __FreeBSD__
#   define CDDA_DIRECTORY "/cdrom"
#  elif defined __OpenBSD__
#   define CDDA_DIRECTORY "/cdrom"
#  else
#   define CDDA_DIRECTORY "/cdrom/cdrom0"
#  endif
# else
#   define CDDA_DIRECTORY "/mnt/cdrom"
# endif
#endif

static char * cdda_get_title(cdda_disc_toc_t *toc, int track);
static gboolean stop_timeout(gpointer data);

static void cdda_init(void);
static int is_our_file(char *filename);
static GList *scan_dir(char *dir);
static void play_file(char *filename);
static void stop(void);
static void cdda_pause(short p);
static void seek(int time);
static int get_time(void);
static void get_song_info(char *filename, char **title, int *length);
static void get_volume(int *l, int *r);
static void set_volume(int l, int r);
static void cleanup(void);
void cdda_fileinfo(char *filename);

InputPlugin cdda_ip =
{
	NULL,
	NULL,
	NULL, /* Description */
	cdda_init,
	NULL,				/* about */
	cdda_configure,
	is_our_file,
	scan_dir,
	play_file,
	stop,
	cdda_pause,
	seek,
	NULL,				/* set_eq */
	get_time,
	get_volume,
	set_volume,
	cleanup,
	NULL,				/* obsolete */
	NULL,				/* add_vis_pcm */
	NULL,				/* set_info, filled in by xmms */
	NULL,				/* set_info_text, filled in by xmms */
	get_song_info,
	NULL, /*  cdda_fileinfo, */	/* file_info_box */
	NULL				/* output plugin handle */
};

CDDAConfig cdda_cfg;

static struct {
	struct driveinfo drive;
	cdda_disc_toc_t cd_toc;
	int track;
	int fd;
	gboolean playing;
} cdda_playing;

static struct {
	pthread_t thread;
	gboolean audio_error, eof;
	int seek;
	
} dae_data;

static gboolean is_paused;
static int pause_time;

struct timeout {
	int id;
	char *device;
};

static GList *timeout_list;

/* Time to delay stop command in 1/10 second */
#define STOP_DELAY 20

InputPlugin *get_iplugin_info(void)
{
	cdda_ip.description = g_strdup_printf(_("CD Audio Player %s"), VERSION);
	return &cdda_ip;
}

static void cdda_init(void)
{
	ConfigFile *cfgfile;
	struct driveinfo *drive = g_malloc0(sizeof(struct driveinfo));
	int ndrives = 1, i;

	cdda_playing.fd = -1;
	memset(&cdda_cfg, 0, sizeof(CDDAConfig));

#ifdef HAVE_OSS
  	drive->mixer = CDDA_MIXER_OSS;
	drive->oss_mixer = SOUND_MIXER_CD;
#endif

	cfgfile = xmms_cfg_open_default_file();

	/* These names are used for backwards compatibility */
	xmms_cfg_read_string(cfgfile, "CDDA", "device", &drive->device);
	xmms_cfg_read_string(cfgfile, "CDDA", "directory", &drive->directory);
	xmms_cfg_read_int(cfgfile, "CDDA", "mixer", &drive->mixer);
	xmms_cfg_read_int(cfgfile, "CDDA", "readmode", &drive->dae);

	if (!drive->device)
		drive->device = g_strdup(CDDA_DEVICE);
	if (!drive->directory)
		drive->directory = g_strdup(CDDA_DIRECTORY);

	cdda_cfg.drives = g_list_append(cdda_cfg.drives, drive);

	xmms_cfg_read_int(cfgfile, "CDDA", "num_drives", &ndrives);
  	for (i = 1; i < ndrives; i++)
	{
		char label[20];
		drive = g_malloc0(sizeof(struct driveinfo));

		sprintf(label, "device%d", i);
		xmms_cfg_read_string(cfgfile, "CDDA", label, &drive->device);
		
		sprintf(label, "directory%d", i);
		xmms_cfg_read_string(cfgfile, "CDDA", label, &drive->directory);

		sprintf(label, "mixer%d", i);
		xmms_cfg_read_int(cfgfile, "CDDA", label, &drive->mixer);

		sprintf(label, "readmode%d", i);
		xmms_cfg_read_int(cfgfile, "CDDA", label, &drive->dae);

		cdda_cfg.drives = g_list_append(cdda_cfg.drives, drive);
	}
	xmms_cfg_read_boolean(cfgfile, "CDDA", "title_override", &cdda_cfg.title_override);
	xmms_cfg_read_string(cfgfile, "CDDA", "name_format", &cdda_cfg.name_format);
	xmms_cfg_read_boolean(cfgfile, "CDDA", "use_cddb", &cdda_cfg.use_cddb);
	xmms_cfg_read_string(cfgfile, "CDDA", "cddb_server", &cdda_cfg.cddb_server);
#ifdef WITH_CDINDEX
	xmms_cfg_read_boolean(cfgfile, "CDDA", "use_cdin", &cdda_cfg.use_cdin);
#else
	cdda_cfg.use_cdin = FALSE;
#endif
	xmms_cfg_read_string(cfgfile, "CDDA", "cdin_server", &cdda_cfg.cdin_server);
	xmms_cfg_free(cfgfile);

	if (!cdda_cfg.cdin_server)
		cdda_cfg.cdin_server = g_strdup("www.cdindex.org");
	if (!cdda_cfg.cddb_server)
		cdda_cfg.cddb_server = g_strdup(CDDB_DEFAULT_SERVER);
	if (!cdda_cfg.name_format)
		cdda_cfg.name_format = g_strdup("%p - %t");
}

struct driveinfo* cdda_find_drive(char *filename)
{
	GList *node;
	
	for (node = cdda_cfg.drives; node; node = node->next)
	{
		struct driveinfo *d = node->data;
		if (!strncmp(d->directory, filename, strlen(d->directory)))
			return d;
	}

	return NULL;
}

static void timeout_destroy(struct timeout *entry)
{
	g_free(entry->device);
	g_free(entry);
	timeout_list = g_list_remove(timeout_list, entry);
}

static void timeout_remove_for_device(char *device)
{
	GList *node;

	for (node = timeout_list; node; node = node->next)
	{
		struct timeout *t = node->data;

		if (!strcmp(t->device, device))
		{
			gtk_timeout_remove(t->id);
			timeout_destroy(t);
			return;
		}
	}

}

static void cleanup(void)
{
	while (timeout_list)
	{
		struct timeout *t = timeout_list->data;
		gtk_timeout_remove(t->id);
		stop_timeout(t);
	}
	cddb_quit();
}

static int is_our_file(char *filename)
{
	char *ext;

	if (cdda_find_drive(filename) == NULL)
		return FALSE;

	ext = strrchr(filename, '.');
	if (ext && !strcasecmp(ext, ".cda"))
		return TRUE;

	return FALSE;
}

static gboolean is_mounted(char * device_name)
{
#if defined(HAVE_MNTENT_H) || defined(HAVE_GETMNTINFO)
  	char devname[256];
  	struct stat st;
#if defined(HAVE_MNTENT_H)
 	FILE *mounts;
 	struct mntent *mnt;
#elif defined(HAVE_GETMNTINFO)
 	struct statfs *fsp;
 	int entries;
#endif

	if (lstat(device_name, &st) < 0)
		return FALSE;

	if (S_ISLNK(st.st_mode))
		readlink(device_name, devname, 256);
	else
		strncpy(devname, device_name, 256);

#if defined(HAVE_MNTENT_H)		
	if ((mounts = setmntent(MOUNTED, "r")) == NULL)
		return TRUE;

	while ((mnt = getmntent(mounts)) != NULL)
	{
		if (strcmp(mnt->mnt_fsname, devname) == 0)
		{
			endmntent(mounts);
			return TRUE;
		}
	}
	endmntent(mounts);
#elif defined(HAVE_GETMNTINFO)
 	entries = getmntinfo(&fsp, MNT_NOWAIT);
 	if (entries < 0)
 		return FALSE;
	
	while (entries-- > 0)
	{
		if (!strcmp(fsp->f_mntfromname, devname))
			return TRUE;
		fsp++;
	}
#endif                            
#endif
	return FALSE;
}

static GList *scan_dir(char *dir)
{
	GList *list = NULL;
	int i;
	cdda_disc_toc_t toc;
	struct driveinfo *drive;

	if ((drive = cdda_find_drive(dir)) == NULL)
		return NULL;

	if (!cdda_get_toc(&toc, drive->device))
		return NULL;

	for (i = toc.last_track; i >= toc.first_track; i--)
		if (!toc.track[i].flags.data_track)
			list = g_list_prepend(list, g_strdup_printf("Track %02d.cda", i));

	return list;
}

guint cdda_calculate_track_length(cdda_disc_toc_t *toc, int track)
{
	if (track == toc->last_track)
		return (LBA(toc->leadout) - LBA(toc->track[track]));
	else
		return (LBA(toc->track[track + 1]) - LBA(toc->track[track]));
}

static void *dae_play_loop(void *arg)
{
	char *buffer = g_malloc(CD_FRAMESIZE_RAW * CDDA_DAE_FRAMES);
	int pos = LBA(cdda_playing.cd_toc.track[cdda_playing.track]);
	int end, frames;

	if (cdda_playing.track == cdda_playing.cd_toc.last_track)
		end = LBA(cdda_playing.cd_toc.leadout);
	else
		end = LBA(cdda_playing.cd_toc.track[cdda_playing.track + 1]);

	while (cdda_playing.playing)
	{
		int left;
		char *data;

		if (dae_data.seek != -1)
		{
			cdda_ip.output->flush(dae_data.seek * 1000);
			pos = LBA(cdda_playing.cd_toc.track[cdda_playing.track])
				+ dae_data.seek * 75;
			dae_data.seek = -1;
			dae_data.eof = FALSE;
		}
		frames = MIN(CDDA_DAE_FRAMES, end - pos);
		if (frames == 0)
			dae_data.eof = TRUE;

		if (dae_data.eof)
		{
			xmms_usleep(30000);
			continue;
		}

		frames = read_audio_data(cdda_playing.fd, pos,
					 frames, buffer);
		if (frames <= 0)
		{
			int err = -frames;
			if (err == EOPNOTSUPP)
				dae_data.eof = TRUE;
			else
			{
				/*
				 * If the read failed, skip ahead to
				 * avoid getting stuck on scratches
				 * and such.
				 */
				g_message("read_audio_data() failed:  %s (%d)",
					  strerror(err), err);
				pos += MIN(CDDA_DAE_FRAMES, end - pos);
			}
			continue;
		}
		left = frames * CD_FRAMESIZE_RAW;
		data = buffer;
		while (cdda_playing.playing && left > 0 && dae_data.seek == -1)
		{
			int cur = MIN(512 * 2 * 2, left);
			cdda_ip.add_vis_pcm(cdda_ip.output->written_time(),
					    FMT_S16_LE, 2, cur, data);
			while (cdda_ip.output->buffer_free() < cur &&
			       cdda_playing.playing && dae_data.seek == -1)
				xmms_usleep(30000);
			if (cdda_playing.playing && dae_data.seek == -1)
				cdda_ip.output->write_audio(data, cur);
			left -= cur;
			data += cur;
		}
		pos += frames;
	}

	cdda_ip.output->buffer_free();
	cdda_ip.output->buffer_free();
	g_free(buffer);

	return NULL;
}

static void dae_play(void)
{
	if (cdda_ip.output->open_audio(FMT_S16_LE, 44100, 2) == 0)
	{
		dae_data.audio_error = TRUE;
		cdda_playing.playing = FALSE;
		return;
	}
	dae_data.seek = -1;
	dae_data.eof = FALSE;
	dae_data.audio_error = FALSE;
	pthread_create(&dae_data.thread, NULL, dae_play_loop, NULL);
}

static void play_file(char *filename)
{
	char *tmp;
	struct driveinfo *drive;
	int track;
	int track_len;

	if ((drive = cdda_find_drive(filename)) == NULL)
		return;

	if (is_mounted(drive->device))
		return;

	tmp = strrchr(filename, '/');
	if (tmp)
		tmp++;
	else
		tmp = filename;

	if (!sscanf(tmp, "Track %d.cda", &track))
		return;
	if (track < 0 || track > 99)
		return;

	if (!cdda_get_toc(&cdda_playing.cd_toc, drive->device) ||
	    cdda_playing.cd_toc.track[track].flags.data_track ||
	    track < cdda_playing.cd_toc.first_track ||
	    track > cdda_playing.cd_toc.last_track)
		return;

	if ((cdda_playing.fd = open(drive->device, CDOPENFLAGS)) == -1)
		return;

	track_len = cdda_calculate_track_length(&cdda_playing.cd_toc, track);
	cdda_ip.set_info(cdda_get_title(&cdda_playing.cd_toc, track),
			 (track_len  * 1000) / 75, 44100 * 2 * 2 * 8, 44100, 2);

	memcpy(&cdda_playing.drive, drive, sizeof (struct driveinfo)); 
#ifndef CDDA_HAS_READAUDIO
	cdda_playing.drive.dae = FALSE;
#endif

	cdda_playing.track = track;

	is_paused = FALSE;
	timeout_remove_for_device(drive->device);

	cdda_playing.playing = TRUE;
	if (drive->dae)
		dae_play();
	else
		seek(0);
}

static char * cdda_get_title(cdda_disc_toc_t *toc, int track)
{
	static guint32 cached_id;
	static cdinfo_t cdinfo;
	static pthread_mutex_t title_mutex = PTHREAD_MUTEX_INITIALIZER;
	TitleInput *input;
	guint32 disc_id;
	char *title;

	disc_id = cdda_cddb_compute_discid(toc);
	
	/*
	 * We want to avoid looking up a album from two threads simultaneously.
	 * This can happen since we are called both from the main-thread and
	 * from the playlist-thread.
	 */
	
	pthread_mutex_lock(&title_mutex);
	if (!(disc_id == cached_id && cdinfo.is_valid))
	{
		/*
		 * We try to look up the disc again if the info is not
		 * valid.  The user might have configured a new server
		 * in the meantime.
		 */
		cdda_cdinfo_flush(&cdinfo);
		cached_id = disc_id;

		if (!cdda_cdinfo_read_file(disc_id, &cdinfo))
		{
			if (cdda_cfg.use_cddb)
				cdda_cddb_get_info(toc, &cdinfo);
			if (cdinfo.is_valid)
				cdda_cdinfo_write_file(disc_id, &cdinfo);
		}
	}
	XMMS_NEW_TITLEINPUT(input);
	cdda_cdinfo_get(&cdinfo, track, &input->performer, &input->album_name,
			&input->track_name);
	pthread_mutex_unlock(&title_mutex);
	
	input->track_number = track;
	input->file_name = input->file_path = 
	    g_strdup_printf(_("CD Audio Track %02u"), track);
	input->file_ext = "cda";
	title =  xmms_get_titlestring(cdda_cfg.title_override ?
				      cdda_cfg.name_format :
				      xmms_get_gentitle_format(), input);
	g_free(input->file_name);
	g_free(input);

	if (!title)
		title = g_strdup_printf(_("CD Audio Track %02u"), track);
	return title;
}

static gboolean stop_timeout(gpointer data)
{
	int fd;
	struct timeout *to = data;

	fd = open(to->device, CDOPENFLAGS);
	if (fd != -1)
	{
		ioctl(fd, XMMS_STOP, 0);
		close(fd);
	}
	timeout_destroy(to);
	return FALSE;
}

static void stop(void)
{
	struct timeout *to_info;
	if (cdda_playing.fd < 0)
		return;

	cdda_playing.playing = FALSE;
	
	if (cdda_playing.drive.dae)
	{
		pthread_join(dae_data.thread, NULL);
		cdda_ip.output->close_audio();
	}
	else
		ioctl(cdda_playing.fd, XMMS_PAUSE, 0);

	close(cdda_playing.fd);
	cdda_playing.fd = -1;

	if (!cdda_playing.drive.dae)
	{
		to_info = g_malloc(sizeof (*to_info));
		to_info->device = g_strdup(cdda_playing.drive.device);
		to_info->id = gtk_timeout_add(STOP_DELAY * 100, stop_timeout,
					      to_info);
		timeout_list = g_list_prepend(timeout_list, to_info);
	}
}

static void cdda_pause(short p)
{
	if (cdda_playing.drive.dae)
	{
		cdda_ip.output->pause(p);
		return;
	}
	if (p)
	{
		pause_time = get_time();
		ioctl(cdda_playing.fd, XMMS_PAUSE, 0);
	}
	else
	{
		ioctl(cdda_playing.fd, XMMS_RESUME, 0);
		pause_time = -1;
	}
	is_paused = p;
}

#ifdef XMMS_CDROM_SOLARIS
/*
 * Lowlevel cdrom access, Solaris style (Solaris, Linux)
 */

static void play_ioctl(struct cdda_msf *start, struct cdda_msf *end)
{
	struct cdrom_msf msf;
	
	msf.cdmsf_min0 = start->minute;
	msf.cdmsf_sec0 = start->second;
	msf.cdmsf_frame0 = start->frame;
	msf.cdmsf_min1 = end->minute;
	msf.cdmsf_sec1 = end->second;
	msf.cdmsf_frame1 = end->frame;
	ioctl(cdda_playing.fd, CDROMPLAYMSF, &msf);
}

static int get_current_frame(void)
{
	struct cdrom_subchnl subchnl;

	subchnl.cdsc_format = CDROM_MSF;
	if (ioctl(cdda_playing.fd, CDROMSUBCHNL, &subchnl) < 0)
		return -1;

	switch (subchnl.cdsc_audiostatus)
	{
		case CDROM_AUDIO_COMPLETED:
		case CDROM_AUDIO_ERROR:
			return -1;
	}

	return (LBA(subchnl.cdsc_absaddr.msf));
}

#if !defined(CDROMVOLREAD)
static int volume_left = 100, volume_right = 100;
#endif

static void drive_get_volume(int *l, int *r)
{
#if defined(CDROMVOLREAD)
	struct cdrom_volctrl vol;

	if (cdda_playing.fd!=-1 && !ioctl(cdda_playing.fd, CDROMVOLREAD, &vol))
	{
		*l = (100 * vol.channel0) / 255;
		*r = (100 * vol.channel1) / 255;
	}
#if 0
	else if (cdda_playing.fd != -1)
		g_message("CDROMVOLREAD failed");
#endif
#else
	*l = volume_left;
	*r = volume_right;
#endif
}

static void drive_set_volume(int l, int r)
{
	struct cdrom_volctrl vol;

	if (cdda_playing.fd != -1)
	{
		vol.channel0 = vol.channel2 = (l * 255) / 100;
		vol.channel1 = vol.channel3 = (r * 255) / 100;
		ioctl(cdda_playing.fd, CDROMVOLCTRL, &vol);
	}
#if !defined(CDROMVOLREAD)
	volume_left = l;
	volume_right = r;
#endif
}

#ifdef CDROMREADAUDIO
int read_audio_data(int fd, int pos, int num, void *buf)
{
	struct cdrom_read_audio cdra;

#if 1
	cdra.addr.lba = pos - CDDA_MSF_OFFSET;
	cdra.addr_format = CDROM_LBA;
#else	
	cdra.addr.msf.minute = pos / (60 * 75);
	cdra.addr.msf.second = (pos / 75) % 60;
	cdra.addr.msf.frame = pos % 75;
	cdra.addr_format = CDROM_MSF;
#endif
	
	cdra.nframes = num;
	cdra.buf = buf;

	if (ioctl(fd, CDROMREADAUDIO, &cdra) < 0)
		return -errno;

	return cdra.nframes;
}
#endif /* CDROMREADAUDIO */

#if defined(CDROMCDDA)
int read_audio_data(int fd, int pos, int num, void *buf)
{
	struct cdrom_cdda cdra;

	cdra.cdda_addr = pos - CDDA_MSF_OFFSET;
	cdra.cdda_length = num;
	cdra.cdda_data = buf;
	cdra.cdda_subcode = CDROM_DA_NO_SUBCODE;
	if (ioctl(fd, CDROMCDDA, &cdra) < 0)
		return -errno;

	return cdra.cdda_length;
}
#endif

static gboolean cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t *info)
{
	struct cdrom_tochdr tochdr;
	struct cdrom_tocentry tocentry;
	int i;

	if (ioctl(fd, CDROMREADTOCHDR, &tochdr))
		return FALSE;

	for (i = tochdr.cdth_trk0; i <= tochdr.cdth_trk1; i++)
	{		
		tocentry.cdte_format = CDROM_MSF;
		tocentry.cdte_track = i;
		if (ioctl(fd, CDROMREADTOCENTRY, &tocentry))
			return FALSE;
		info->track[i].minute = tocentry.cdte_addr.msf.minute;
		info->track[i].second = tocentry.cdte_addr.msf.second;
		info->track[i].frame = tocentry.cdte_addr.msf.frame;
		info->track[i].flags.data_track =
			tocentry.cdte_ctrl == CDROM_DATA_TRACK;
	}

	/* Get the leadout track */
	tocentry.cdte_track = CDROM_LEADOUT;
	tocentry.cdte_format = CDROM_MSF;
	if (ioctl(fd, CDROMREADTOCENTRY, &tocentry))
		return FALSE;
	info->leadout.minute = tocentry.cdte_addr.msf.minute;
	info->leadout.second = tocentry.cdte_addr.msf.second;
	info->leadout.frame = tocentry.cdte_addr.msf.frame;
	
	info->first_track = tochdr.cdth_trk0;
	info->last_track = tochdr.cdth_trk1;

	return TRUE;
}

#endif

#ifdef XMMS_CDROM_BSD
/*
 * Lowlevel cdrom access, BSD style (FreeBSD, OpenBSD, NetBSD, Darwin)
 */

static void play_ioctl(struct cdda_msf *start, struct cdda_msf *end)
{
	struct ioc_play_msf msf;

	msf.start_m = start->minute;
	msf.start_s = start->second;
	msf.start_f = start->frame;
	msf.end_m = end->minute;
	msf.end_s = end->second;
	msf.end_f = end->frame;
	ioctl(cdda_playing.fd, CDIOCPLAYMSF, &msf);
}

static int get_current_frame(void)
{
	struct ioc_read_subchannel subchnl;
	struct cd_sub_channel_info subinfo;
	subchnl.address_format = CD_MSF_FORMAT;
	subchnl.data_format = CD_CURRENT_POSITION;
	subchnl.track = 0;
	subchnl.data_len = sizeof(subinfo);
	subchnl.data = &subinfo;
	if (ioctl(cdda_playing.fd, CDIOCREADSUBCHANNEL, &subchnl) < 0)
		return -1;

#ifdef XMMS_CDROM_BSD_DARWIN
	return ((subchnl.data->what.position.absaddr[1] * 60
		 subchnl.data->what.position.absaddr[2]) * 75 +
		subchnl.data->what.position.absaddr[3]);
#else
	return (LBA(subchnl.data->what.position.absaddr.msf));
#endif
}

static void drive_get_volume(int *l, int *r)
{
	struct ioc_vol vol;
	
	if (cdda_playing.fd != -1)
	{
		ioctl(cdda_playing.fd, CDIOCGETVOL, &vol);
		*l = (100 * vol.vol[0]) / 255;
		*r = (100 * vol.vol[1]) / 255;
	}
}

static void drive_set_volume(int l, int r)
{
	struct ioc_vol vol;

	if (cdda_playing.fd != -1)
	{
		vol.vol[0] = vol.vol[2] = (l * 255) / 100;
		vol.vol[1] = vol.vol[3] = (r * 255) / 100;
		ioctl(cdda_playing.fd, CDIOCSETVOL, &vol);
	}
}


#ifdef __FreeBSD__
# if defined(CDIOCREADAUDIO)
/* Digital extraction before ATAng */
int read_audio_data(int fd, int pos, int num, void *buf)
{
	struct ioc_read_audio cdra;

	cdra.address.lba = pos - CDDA_MSF_OFFSET;
	cdra.address_format = CD_LBA_FORMAT;
	cdra.nframes = num;
	cdra.buffer = buf;

	if (ioctl(fd, CDIOCREADAUDIO, &cdra) < 0)
		return -errno;

	return cdra.nframes;
}
# else
/* Digital extraction with ATAng */
int read_audio_data(int fd, int pos, int num, void *buf)
{	
	int bsize = 2352;

	if (ioctl(fd, CDRIOCSETBLOCKSIZE, &bsize) == -1)
		return -errno;
	if (pread(fd, buf, num*bsize, (pos - 150)*bsize) != num*bsize)
		return 0;
	return num;	 	
}
# endif /* CDIOCREADAUDIO */
#endif

#ifdef XMMS_CDROM_BSD_NETBSD /* NetBSD, OpenBSD */

static gboolean cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t *info)
{
	struct ioc_toc_header tochdr;
	struct ioc_read_toc_entry tocentry;
	struct cd_toc_entry tocentrydata;
	int i;

	if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
		return FALSE;

	for (i = tochdr.starting_track; i <= tochdr.ending_track; i++)
	{		
		tocentry.address_format = CD_MSF_FORMAT;

		tocentry.starting_track = i;
		tocentry.data = &tocentrydata;
		tocentry.data_len = sizeof(tocentrydata);
		if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
			return FALSE;
		info->track[i].minute = tocentry.data->addr.msf.minute;
		info->track[i].second = tocentry.data->addr.msf.second;
		info->track[i].frame = tocentry.data->addr.msf.frame;
		info->track[i].flags.data_track =
		    (tocentry.data->control & 4) == 4;
	}

	/* Get the leadout track */
	tocentry.address_format = CD_MSF_FORMAT;

	tocentry.starting_track = 0xAA;
	tocentry.data = &tocentrydata;
	tocentry.data_len = sizeof(tocentrydata);
	if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
		return FALSE;
	info->leadout.minute = tocentry.data->addr.msf.minute;
	info->leadout.second = tocentry.data->addr.msf.second;
	info->leadout.frame = tocentry.data->addr.msf.frame;
	
	info->first_track = tochdr.starting_track;
	info->last_track = tochdr.ending_track;

	return TRUE;
}

#elif defined(XMMS_CDROM_BSD_DARWIN)

static gboolean cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t *info)
{
	struct ioc_toc_header tochdr;
	struct ioc_read_toc_entry tocentry;
	int i;

	if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
		return FALSE;

	for (i = tochdr.starting_track; i <= tochdr.ending_track; i++)
	{		
		tocentry.address_format = CD_MSF_FORMAT;

		tocentry.starting_track = i;
		if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
			return FALSE;
		info->track[i].minute =	tocentry.data->addr[1];
		info->track[i].second = tocentry.data->addr[2];
		info->track[i].frame = tocentry.data->addr[3];
		info->track[i].flags.data_track =
			(tocentry.data->control & 4) == 4;
	}

	/* Get the leadout track */
	tocentry.address_format = CD_MSF_FORMAT;

	tocentry.starting_track = 0xAA;
	if (ioctl(fd, CDIOREADTOCENTRYS, &tocentry))
		return FALSE;
	info->leadout.minute = tocentry.data->addr[1];
	info->leadout.second = tocentry.data->addr[2];
	info->leadout.frame = tocentry.data->addr[3];

	return TRUE;
}

#else /* FreeBSD */

static gboolean cdda_get_toc_lowlevel(int fd, cdda_disc_toc_t *info)
{
	struct ioc_toc_header tochdr;
	struct ioc_read_toc_single_entry tocentry;
	int i;

	if (ioctl(fd, CDIOREADTOCHEADER, &tochdr))
		return FALSE;

	for (i = tochdr.starting_track; i <= tochdr.ending_track; i++)
	{		
		tocentry.address_format = CD_MSF_FORMAT;

		tocentry.track = i;
		if (ioctl(fd, CDIOREADTOCENTRY, &tocentry))
			return FALSE;
		info->track[i].minute = tocentry.entry.addr.msf.minute;
		info->track[i].second = tocentry.entry.addr.msf.second;
		info->track[i].frame = tocentry.entry.addr.msf.frame;
		info->track[i].flags.data_track =
		    (tocentry.entry.control & 4) == 4;
	}

	/* Get the leadout track */
	tocentry.address_format = CD_MSF_FORMAT;

	tocentry.track = 0xAA;
	if (ioctl(fd, CDIOREADTOCENTRY, &tocentry))
		return FALSE;
	info->leadout.minute = tocentry.entry.addr.msf.minute;
	info->leadout.second = tocentry.entry.addr.msf.second;
	info->leadout.frame = tocentry.entry.addr.msf.frame;
	
	info->first_track = tochdr.starting_track;
	info->last_track = tochdr.ending_track;

	return TRUE;
}
#endif

#endif


static void seek(int time)
{
	struct cdda_msf *end, start;
	int track = cdda_playing.track;

	if (cdda_playing.drive.dae)
	{
		dae_data.seek = time;
		while (dae_data.seek != -1)
			xmms_usleep(20000);
		return;
	}
	
	start.minute = (cdda_playing.cd_toc.track[track].minute * 60 +
			cdda_playing.cd_toc.track[track].second + time) / 60;
	start.second = (cdda_playing.cd_toc.track[track].second + time) % 60;
	start.frame = cdda_playing.cd_toc.track[track].frame;
	if (track == cdda_playing.cd_toc.last_track)
		end = &cdda_playing.cd_toc.leadout;
	else
		end = &cdda_playing.cd_toc.track[track + 1];

	play_ioctl(&start, end);
	
	if (is_paused)
	{
		cdda_pause(TRUE);
		pause_time = time * 1000;
	}
}

static int get_time_analog(void)
{
	int frame, start_frame, length;
	int track = cdda_playing.track;

	if (is_paused && pause_time != -1)
		return pause_time;

	frame = get_current_frame();

	if (frame == -1)
		return -1;
	
	start_frame = LBA(cdda_playing.cd_toc.track[track]);
	length = cdda_calculate_track_length(&cdda_playing.cd_toc, track);

	if (frame - start_frame >= length - 20)	/* 20 seems to work better */
		return -1;

	return ((frame - start_frame) * 1000) / 75;
}

static int get_time_dae(void)
{
	if (dae_data.audio_error)
		return -2;
	if (!cdda_playing.playing ||
	    (dae_data.eof && !cdda_ip.output->buffer_playing()))
		return -1;
	return cdda_ip.output->output_time();
}

static int get_time(void)
{
	if (cdda_playing.fd == -1)
		return -1;

	if (cdda_playing.drive.dae)
		return get_time_dae();
	else
		return get_time_analog();
}

static void get_song_info(char *filename, char **title, int *len)
{
	cdda_disc_toc_t toc;
	int t;
	char *tmp;
	struct driveinfo *drive;

	*title = NULL;
	*len = -1;

	if ((drive = cdda_find_drive(filename)) == NULL)
		return;

	tmp = strrchr(filename, '/');
	if (tmp)
		tmp++;
	else
		tmp = filename;
	
	if (!sscanf(tmp, "Track %d.cda", &t))
		return;
	if (!cdda_get_toc(&toc, drive->device))
		return;
	if (t < toc.first_track || t > toc.last_track || toc.track[t].flags.data_track)
		return;
	
	*len = (cdda_calculate_track_length(&toc, t) * 1000) / 75;
	*title = cdda_get_title(&toc, t);
}

#ifdef HAVE_OSS
static void oss_get_volume(int *l, int *r, int mixer_line)
{
	int fd, v;

	fd = open(DEV_MIXER, O_RDONLY);
	if (fd != -1)
	{
		ioctl(fd, MIXER_READ(mixer_line), &v);
		*r = (v & 0xFF00) >> 8;
		*l = (v & 0x00FF);
		close(fd);
	}
}

static void oss_set_volume(int l, int r, int mixer_line)
{
	int fd, v;

	fd = open(DEV_MIXER, O_RDONLY);
	if (fd != -1)
	{
		v = (r << 8) | l;
		ioctl(fd, MIXER_WRITE(mixer_line), &v);
		close(fd);
	}
}
#else
static void oss_get_volume(int *l, int *r, int mixer_line)
{}
static void oss_set_volume(int l, int r, int mixer_line)
{}
#endif


static void get_volume(int *l, int *r)
{
	if (cdda_playing.drive.dae)
	{
		if (cdda_ip.output->get_volume)
			cdda_ip.output->get_volume(l, r);
	}
	else if (cdda_playing.drive.mixer == CDDA_MIXER_OSS)
		oss_get_volume(l, r, cdda_playing.drive.oss_mixer);
	else if(cdda_playing.drive.mixer == CDDA_MIXER_DRIVE)
		drive_get_volume(l, r);
}

static void set_volume(int l, int r)
{
	if (cdda_playing.drive.dae)
		cdda_ip.output->set_volume(l, r);
	else if (cdda_playing.drive.mixer == CDDA_MIXER_OSS)
		oss_set_volume(l, r, cdda_playing.drive.oss_mixer);
	else if (cdda_playing.drive.mixer == CDDA_MIXER_DRIVE)
		drive_set_volume(l, r);
}

gboolean cdda_get_toc(cdda_disc_toc_t *info, char *device)
{
	gboolean retv = FALSE;
	int fd;

	if (is_mounted(device))
		return FALSE;

	if ((fd = open(device, CDOPENFLAGS)) == -1)
		return FALSE;

	memset(info, 0, sizeof(cdda_disc_toc_t));
	retv = cdda_get_toc_lowlevel(fd, info);
	close(fd);

	return retv;
}
